📚 Professor Application Tracker

☁️ 跨设备同步
部署完 Worker 后填 URL + 选一个 handle (像 "yj2026"),所有设备填同样的就同步阅读/星标/对话历史。

💬 LLM 问答

问任何教授策略 / 邮件起草 / 论文总结 / 方向选择。会自动把"当前正在精读的那位教授"的档案塞进 context。

📖 逐位精读 · 进度跟踪

📊 子方向统计

每个方向的老师数 + 在招比例 + 平均发文难度。可帮你看哪个方向 supply 多。

🎯 已申请 / 进行中

🚀 上升期 PI — 暑假大概率在校,急招学生

自动筛选:assistant prof / 2022-2026 新入职 / lab 小 / 明确 OPEN_2026 / 没有 sabbatical 信号

策略:大牛(Levine/Song Han/Levine 类)暑假经常在外,初级 PI 反而 100% 在 lab + 抢学生抢得凶。优先这类。

🌳 研究方向树(思维导图)

每个方向 → 细分领域 → 老师。点 prof 名字看完整档案。

🔍 按研究方向分类(含已申请 + 待调查)

点击任意卡片看完整档案 · 招生 badge 显示是否官方在招 · 🌟 = Codex 4/14 推荐

🇨🇳 国内 CS 顶尖教授(清北 / 交大 / 浙大 / 复旦 / 中科大 / HKUST / CUHK / HKU)

覆盖国内做 AI Agents / World Model / Robotics / LLM / Edge AI / Medical 等热门方向的 PI。逐步填充。

🏆 全球顶会一览

Tier S+ / S / A / A+ 划分,按方向分组。趋势栏写当下 hot topic。

📄 重要论文榜(2025 / 2026)

按方向 → 年份分组。点链接看 arxiv / project page。慢慢补到 200+ 篇。

🏭 工业界 / 学术界重要发布(2025–2026)

GPT-5 / Claude / Gemini / Llama / Qwen / DeepSeek / Kimi 等模型 + 重要实验室动态。

⚖️ 中外对比:方向逐项分析

每个方向:美国领先点 / 国内领先点 / 差距 / 双方代表作。

function getSyncAPI() { return localStorage.getItem(SYNC_API_KEY) || SYNC_API_DEFAULT || ''; } function getSyncHandle() { return localStorage.getItem(SYNC_HANDLE_KEY) || ''; } function setSyncHandle(h) { localStorage.setItem(SYNC_HANDLE_KEY, h); } function setSyncAPI(u) { localStorage.setItem(SYNC_API_KEY, u); } async function cloudPull() { const api = getSyncAPI(), h = getSyncHandle(); if (!api || !h) return false; try { const r = await fetch(api + '/state?id=' + encodeURIComponent(h), { cache:'no-store' }); if (!r.ok) { console.warn('[sync] pull failed', r.status); return false; } const data = await r.json(); if (data && data.ts) { const localTs = parseInt(localStorage.getItem('prof_tracker_local_ts')||'0', 10); // If cloud is newer than local, overwrite local if (data.ts > localTs) { if (data.read) localStorage.setItem('prof_tracker_read_v1', JSON.stringify(data.read)); if (data.star) localStorage.setItem('prof_tracker_star_v1', JSON.stringify(data.star)); if (data.chat) localStorage.setItem('prof_tracker_chat_hist_v1', JSON.stringify(data.chat)); localStorage.setItem('prof_tracker_local_ts', String(data.ts)); return true; } } } catch (e) { console.warn('[sync] pull err', e.message); } return false; } let _pushTimer = null; function cloudPushDebounced() { const api = getSyncAPI(), h = getSyncHandle(); if (!api || !h) return; clearTimeout(_pushTimer); _pushTimer = setTimeout(async () => { const ts = Date.now(); const body = JSON.stringify({ read: JSON.parse(localStorage.getItem('prof_tracker_read_v1')||'[]'), star: JSON.parse(localStorage.getItem('prof_tracker_star_v1')||'[]'), chat: JSON.parse(localStorage.getItem('prof_tracker_chat_hist_v1')||'[]').slice(-40), ts, }); localStorage.setItem('prof_tracker_local_ts', String(ts)); try { const r = await fetch(api + '/state?id=' + encodeURIComponent(h), { method:'POST', headers:{'Content-Type':'application/json'}, body, }); const statusEl = document.getElementById('sync-status'); if (statusEl) { if (r.ok) statusEl.textContent = '☁️ 已同步 ' + new Date().toLocaleTimeString(); else statusEl.textContent = '⚠️ 同步失败 ' + r.status; } } catch (e) { const statusEl = document.getElementById('sync-status'); if (statusEl) statusEl.textContent = '⚠️ 网络错误'; } }, 1500); } window.cloudPushDebounced = cloudPushDebounced; // Wire sync UI + initial pull. Runs once after DOM is ready (script is at end of body so DOM exists). function initSyncUI() { const apiInp = document.getElementById('sync-api'); const hInp = document.getElementById('sync-handle'); const status = document.getElementById('sync-status'); if (!apiInp) return; apiInp.value = getSyncAPI(); hInp.value = getSyncHandle(); if (getSyncAPI() && getSyncHandle()) { status.textContent = '☁️ 已配置 · 正在拉取…'; cloudPull().then(pulled => { status.textContent = pulled ? '☁️ 拉取完成(云端较新,已覆盖本地)' : '☁️ 已同步(本地最新)'; // Re-render reader + chat with the fresh data try { const readArr = JSON.parse(localStorage.getItem('prof_tracker_read_v1')||'[]'); const starArr = JSON.parse(localStorage.getItem('prof_tracker_star_v1')||'[]'); const chatArr = JSON.parse(localStorage.getItem('prof_tracker_chat_hist_v1')||'[]'); if (typeof readSet !== 'undefined') { readSet.clear(); readArr.forEach(x => readSet.add(x)); } if (typeof starSet !== 'undefined') { starSet.clear(); starArr.forEach(x => starSet.add(x)); } if (typeof chatHistory !== 'undefined') { chatHistory.length = 0; chatHistory.push(...chatArr); } if (typeof refreshReader === 'function') refreshReader(false); if (typeof renderChat === 'function') renderChat(); } catch (e) {} }); } else { status.textContent = '⚪ 未配置(手动同步只在本地)'; } document.getElementById('sync-save').addEventListener('click', () => { const api = apiInp.value.trim().replace(/\/+$/,''); const h = hInp.value.trim(); if (api && !/^https?:\/\//.test(api)) { alert('URL 必须以 http(s):// 开头'); return; } if (h && !/^[A-Za-z0-9_-]{4,40}$/.test(h)) { alert('handle 4-40 字符,只能 A-Z a-z 0-9 _ -'); return; } setSyncAPI(api); setSyncHandle(h); status.textContent = '✅ 已保存,正在拉取…'; cloudPull().then(() => { status.textContent = '☁️ OK'; }); }); document.getElementById('sync-pull-now').addEventListener('click', async () => { status.textContent = '⏳ 拉取中…'; const pulled = await cloudPull(); status.textContent = pulled ? '⬇ 已覆盖本地' : '本地已最新'; location.reload(); }); document.getElementById('sync-push-now').addEventListener('click', () => { clearTimeout(_pushTimer); cloudPushDebounced(); status.textContent = '⏳ 推送中…'; }); } // Defer init — runs after the rest of the script defines readSet/chatHistory. setTimeout(initSyncUI, 0); // ========================================================= // 📖 READER MODE — sequential prof reading with localStorage progress // ========================================================= const READ_KEY = 'prof_tracker_read_v1'; const STAR_KEY = 'prof_tracker_star_v1'; function loadSet(key) { try { return new Set(JSON.parse(localStorage.getItem(key) || '[]')); } catch { return new Set(); } } function saveSet(key, set) { localStorage.setItem(key, JSON.stringify([...set])); if (typeof cloudPushDebounced === 'function') cloudPushDebounced(); } const readSet = loadSet(READ_KEY); const starSet = loadSet(STAR_KEY); // Build the pool — 海外 queue + 国内 chinese_professors + already-applied function buildReaderPool() { const pool = []; if (window.PROFESSORS_DATA) { PROFESSORS_DATA.queue.forEach(p => pool.push({...p, _src:'海外', _key:p.id})); PROFESSORS_DATA.professors.forEach(p => pool.push({...p, _src:'海外', _key:p.id, _applied:true})); } if (window.GLOBAL_DATA && GLOBAL_DATA.chinese_professors) { GLOBAL_DATA.chinese_professors.forEach(p => pool.push({...p, _src:'国内', _key:p.id})); } return pool; } const READER_POOL = buildReaderPool(); // Populate direction filter (function fillDirFilter() { const sel = document.getElementById('reader-filter-dir'); if (!sel) return; const dirs = [...new Set(READER_POOL.map(p => p.primary_direction).filter(Boolean))].sort(); dirs.forEach(d => { const opt = document.createElement('option'); opt.value = d; opt.textContent = d; sel.appendChild(opt); }); })(); let readerState = { index: 0, filtered: [], }; function getReaderFiltered() { const q = (document.getElementById('reader-search')?.value || '').trim().toLowerCase(); const status = document.getElementById('reader-filter-status')?.value || 'all'; const dir = document.getElementById('reader-filter-dir')?.value || ''; const region = document.getElementById('reader-filter-region')?.value || ''; const sort = document.getElementById('reader-sort')?.value || 'summer'; let arr = READER_POOL.slice(); if (status === 'unread') arr = arr.filter(p => !readSet.has(p._key)); else if (status === 'read') arr = arr.filter(p => readSet.has(p._key)); else if (status === 'starred') arr = arr.filter(p => starSet.has(p._key)); if (dir) arr = arr.filter(p => p.primary_direction === dir); if (region) arr = arr.filter(p => p._src === region); if (q) { arr = arr.filter(p => { const blob = ((p.name||'') + ' ' + (p.school||'') + ' ' + (p.department||'') + ' ' + (p.primary_direction||'') + ' ' + (p.research_areas||[]).join(' ') + ' ' + (p.reason_to_apply||p.reason_to_note||'') + ' ' + (p.notable_recent_paper||'') + ' ' + (Array.isArray(p.current_projects)?p.current_projects.join(' '):'')).toLowerCase(); return blob.includes(q); }); } if (sort === 'overall') { arr.sort((a,b) => overallScore(b) - overallScore(a)); } else if (sort === 'summer') { arr.sort((a,b) => summerAvailability(b) - summerAvailability(a) || (a.priority||99) - (b.priority||99)); } else if (sort === 'priority') { arr.sort((a,b) => (a.priority||99) - (b.priority||99) || summerAvailability(b) - summerAvailability(a)); } else if (sort === 'fit') { arr.sort((a,b) => (b.fit_score||0) - (a.fit_score||0)); } else if (sort === 'random') { arr.sort(() => Math.random() - 0.5); } return arr; } function renderReaderProgress() { const total = READER_POOL.length; const read = READER_POOL.filter(p => readSet.has(p._key)).length; const star = READER_POOL.filter(p => starSet.has(p._key)).length; const pct = total ? (read/total*100).toFixed(1) : 0; const elem = document.getElementById('reader-progress'); if (!elem) return; elem.innerHTML = `
${read} / ${total} 已读 ${pct}% ⭐ ${star} 星标 未读 ${total-read} · 海外 ${READER_POOL.filter(p=>p._src==='海外').length} · 国内 ${READER_POOL.filter(p=>p._src==='国内').length}
`; } function renderReaderCard() { const card = document.getElementById('reader-card'); const pos = document.getElementById('reader-pos'); const btnRead = document.getElementById('reader-toggle-read'); const btnStar = document.getElementById('reader-star'); if (!card) return; const arr = readerState.filtered; if (!arr.length) { card.innerHTML = `
🎉 当前筛选下没有教授
换一个 filter 或重置已读试试
`; pos.textContent = ''; return; } if (readerState.index >= arr.length) readerState.index = 0; if (readerState.index < 0) readerState.index = arr.length - 1; const p = arr[readerState.index]; const isRead = readSet.has(p._key); const isStar = starSet.has(p._key); btnRead.textContent = isRead ? '↻ 取消已读 + 下一位' : '✓ 标记已读 + 下一位'; btnRead.style.background = isRead ? '#9aa0a6' : '#22c55e'; btnStar.textContent = isStar ? '★ 已星标' : '⭐ 星标'; pos.textContent = `${readerState.index+1} / ${arr.length}`; const recruit = p.recruiting_status ? `${p.recruiting_status}` : ''; card.innerHTML = `

${isRead?' ':''}${isStar?' ':''}${p.name || p.id}

${p._src==='国内'?'🇨🇳':'🇺🇸'} ${p.school||''}
${p.department||''}
${priorityBadge(p)} ${summerBadge(p)} ${recruit} ${p.priority ? `priority ${p.priority}` : ''} ${p.junior_pi ? `🆕 ${p.pi_start_year||''} junior` : ''}
${p.website ? `
🔗 ${p.website}
` : ''} ${p.email ? `
📧 ${p.email}
` : ''} ${p.research_areas?.length ? `
${p.research_areas.map(a=>`${a}`).join('')}
` : ''} ${p.reason_to_apply || p.reason_to_note ? `
为什么关注
${p.reason_to_apply || p.reason_to_note}
` : ''} ${p.notable_recent_paper ? `
近期重要论文: ${p.notable_recent_paper}
` : ''} ${p.current_projects?.length ? `
当前项目:
` : ''} ${p.key_papers_chronological?.length ? `
📚 时间线 / 代表作
${p.key_papers_chronological.map(x=>x.replace(/^(\d{4}(?:-\d{2})?)/,'$1')).join('
')}
` : ''} ${p.github_repos?.length ? `
⭐ GitHub: ${p.github_repos.join(' · ')}
` : ''} ${p.lab_members?.length ? `
👥 当前 lab: ${p.lab_members.join(' · ')}
` : ''} ${p.christina_overlap?.length ? `
🎯 你的对口点(写邮件用)
` : ''} ${p.cold_email_hooks?.length ? `
✉️ Cold email 钩子(直接粘)
` : ''}
${p.paper_count_2y ? `
📄 ${p.paper_count_2y} 篇/2y
` : ''} ${p.lab_size_estimate ? `
👥 lab ~${p.lab_size_estimate}
` : ''} ${p.project_cycle_estimate ? `
⏱ ${p.project_cycle_estimate}
` : ''} ${p.phd_admission_difficulty ? `
🎓 PhD 难度 ${p.phd_admission_difficulty}/10
` : ''} ${p.paper_publication_difficulty ? `
📝 发文难度 ${p.paper_publication_difficulty}/10
` : ''} ${p.fit_score ? `
🎯 fit ${p.fit_score}/10
` : ''}
${p.funding_signals?.length ? `
💰 ${p.funding_signals.join(' · ')}
` : ''} `; } function refreshReader(resetIdx=true) { readerState.filtered = getReaderFiltered(); if (resetIdx) readerState.index = 0; renderReaderProgress(); renderReaderCard(); } // Event listeners ['reader-search','reader-filter-status','reader-filter-dir','reader-filter-region','reader-sort'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener(el.tagName==='INPUT'?'input':'change', () => refreshReader(true)); }); document.getElementById('reader-prev')?.addEventListener('click', () => { readerState.index--; renderReaderCard(); }); document.getElementById('reader-skip')?.addEventListener('click', () => { readerState.index++; renderReaderCard(); }); document.getElementById('reader-toggle-read')?.addEventListener('click', () => { const p = readerState.filtered[readerState.index]; if (!p) return; if (readSet.has(p._key)) { readSet.delete(p._key); } else { readSet.add(p._key); } saveSet(READ_KEY, readSet); renderReaderProgress(); // 如果当前 filter 是 "仅未读",标记后该 prof 会消失,所以重新筛选;否则只下一位 const status = document.getElementById('reader-filter-status').value; if (status === 'unread' && readSet.has(p._key)) { refreshReader(false); // index 不变,下一位自动顶上 } else { readerState.index++; renderReaderCard(); } }); document.getElementById('reader-star')?.addEventListener('click', () => { const p = readerState.filtered[readerState.index]; if (!p) return; if (starSet.has(p._key)) starSet.delete(p._key); else starSet.add(p._key); saveSet(STAR_KEY, starSet); renderReaderCard(); renderReaderProgress(); }); document.getElementById('reader-reset')?.addEventListener('click', () => { if (confirm(`真的要清空所有 ${readSet.size} 条已读记录吗?`)) { readSet.clear(); saveSet(READ_KEY, readSet); refreshReader(true); } }); // Keyboard shortcuts: J = next, K = prev, R = read, S = star document.addEventListener('keydown', e => { // 如果焦点在输入框就不响应 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return; if (e.key === 'j' || e.key === 'ArrowRight') document.getElementById('reader-skip')?.click(); else if (e.key === 'k' || e.key === 'ArrowLeft') document.getElementById('reader-prev')?.click(); else if (e.key === 'r' || e.key === ' ') { e.preventDefault(); document.getElementById('reader-toggle-read')?.click(); } else if (e.key === 's') document.getElementById('reader-star')?.click(); }); // Initial render refreshReader(true); // ========================================================= // 🔎 RAG INDEX — local corpus over all profs / papers / industry / venues / comparisons // Pure client-side BM25-lite (no embedding API calls). // ========================================================= function buildCorpus() { const docs = []; // profs (queue + applied + 国内) if (window.PROFESSORS_DATA) { [...PROFESSORS_DATA.queue, ...PROFESSORS_DATA.professors].forEach(p => { docs.push({ kind:'prof', id:p.id, region:'海外', title: (p.name||'') + ' @ ' + (p.school||''), body: [ p.name, p.school, p.department, p.primary_direction, (p.research_areas||[]).join(' '), p.reason_to_apply, p.reason_to_note, p.notable_recent_paper, (p.current_projects||[]).join(' '), (p.key_papers_chronological||[]).join(' '), (p.lab_members||[]).join(' '), (p.christina_overlap||[]).join(' '), (p.cold_email_hooks||[]).join(' '), p.recruiting_status, p.email ].filter(Boolean).join(' '), ref: p }); }); } if (window.GLOBAL_DATA?.chinese_professors) { GLOBAL_DATA.chinese_professors.forEach(p => { docs.push({ kind:'prof', id:p.id, region:'国内', title: (p.name||'') + ' @ ' + (p.school||''), body: [ p.name, p.school, p.department, p.primary_direction, (p.research_areas||[]).join(' '), p.reason_to_note, p.notable_recent_paper, (p.current_projects||[]).join(' ') ].filter(Boolean).join(' '), ref: p }); }); } // papers if (window.GLOBAL_DATA?.notable_papers) { GLOBAL_DATA.notable_papers.forEach((p,i) => { docs.push({ kind:'paper', id:'paper_'+i, title: p.title + ' (' + p.venue + ' ' + p.year + ')', body: [ p.title, (p.authors||[]).join(' '), p.institution, p.direction, p.summary, p.why_important, p.venue, p.country, ''+p.year ].filter(Boolean).join(' '), ref: p }); }); } // industry if (window.GLOBAL_DATA?.industry_releases) { GLOBAL_DATA.industry_releases.forEach((r,i) => { docs.push({ kind:'industry', id:'rel_'+i, title: r.org + ' — ' + r.name + ' (' + r.year + '/' + (r.month||'') + ')', body: [r.org, r.name, r.type, r.summary, ''+r.year].filter(Boolean).join(' '), ref: r }); }); } // venues if (window.GLOBAL_DATA?.conferences) { GLOBAL_DATA.conferences.forEach(v => { docs.push({ kind:'venue', id:v.key, title: v.key + ' — ' + v.full, body: [v.key, v.full, v.area, v.tier, v.trend, v.accept_rate].filter(Boolean).join(' '), ref: v }); }); } // comparisons if (window.GLOBAL_DATA?.direction_comparison) { GLOBAL_DATA.direction_comparison.forEach((c,i) => { docs.push({ kind:'comparison', id:'cmp_'+i, title: '中外对比:' + c.direction, body: [c.direction, c.us_strength, c.china_strength, c.gap, (c.examples_us||[]).join(' '), (c.examples_cn||[]).join(' '), c.notes].filter(Boolean).join(' '), ref: c }); }); } return docs; } // Tokenize for BM25 — handles English words + CJK n-grams function tokenize(s) { if (!s) return []; s = s.toLowerCase(); const tokens = []; // English words 3+ chars (and digit groups) const en = s.match(/[a-z0-9][a-z0-9\-+]{1,30}/g) || []; tokens.push(...en); // CJK: extract every contiguous CJK chunk, then make bigrams + unigrams const cjkRuns = s.match(/[一-鿿]+/g) || []; cjkRuns.forEach(run => { for (let i=0; i { // Pre-tokenize all docs once const tokDocs = RAG_CORPUS.map(d => ({ ...d, _tokens: tokenize((d.title + ' ' + d.body)) })); // Compute IDF const df = new Map(); tokDocs.forEach(d => { const seen = new Set(d._tokens); seen.forEach(t => df.set(t, (df.get(t)||0)+1)); }); const N = tokDocs.length; const idf = new Map(); df.forEach((v,k) => idf.set(k, Math.log(1 + (N - v + 0.5)/(v + 0.5)))); // Avg doc length for BM25 const avgdl = tokDocs.reduce((a,d) => a + d._tokens.length, 0) / Math.max(1, tokDocs.length); return { tokDocs, idf, avgdl, N }; })(); function ragSearch(query, k=15) { const qToks = tokenize(query); if (!qToks.length) return []; const k1 = 1.5, b = 0.75; const { tokDocs, idf, avgdl } = RAG_INDEX; const qSet = new Set(qToks); const results = tokDocs.map(d => { const tf = new Map(); d._tokens.forEach(t => { if (qSet.has(t)) tf.set(t, (tf.get(t)||0)+1); }); let score = 0; const dl = d._tokens.length; tf.forEach((freq, t) => { const w = idf.get(t) || 0; score += w * (freq * (k1+1)) / (freq + k1*(1 - b + b*dl/avgdl)); }); // Strong exact-name boost const titleLower = d.title.toLowerCase(); qToks.forEach(t => { if (t.length >= 3 && titleLower.includes(t)) score *= 1.5; }); return { d, score }; }).filter(x => x.score > 0) .sort((a,b) => b.score - a.score) .slice(0, k); return results.map(x => x.d); } window.ragSearch = ragSearch; console.log('[RAG] corpus built:', RAG_CORPUS.length, 'docs ·', RAG_INDEX.N, 'indexed'); // ========================================================= // 💬 LLM CHAT PANEL // OpenAI direct from browser. Key stored in localStorage only. // ========================================================= const CHAT_KEY_STORE = 'prof_tracker_openai_key'; const CHAT_HIST_STORE = 'prof_tracker_chat_hist_v1'; const CHAT_SYS_PROMPT = `你是 Christina Jia 的科研策略助手。她是 Columbia MS CE,GPA 4.0,主要技能: - LLM 微调 (Llama2 PEFT/LoRA — SJTU Apex) - 推理优化 (TensorRT/Jetson — iQIYI + RCRI) - 医疗 AI (J&J chest X-ray) - 当前 advisor: Zhou Yu @ Columbia, 做 Arklex agent eval (Walmart Project 2026) 她正在用一个本地教授追踪工具读 ~713 位教授的档案,准备发 cold email 找 summer/PhD 机会。 回答要:(1) 简洁 (2) 中文为主英文术语保留 (3) 具体可操作 (4) 不要废话。`; function getChatKey() { let k = localStorage.getItem(CHAT_KEY_STORE); if (!k) { k = prompt('请粘贴你的 OpenAI API key (sk-... 或 sk-proj-...):\n会保存到浏览器 localStorage,不会写入文件。'); if (k) { k = k.trim(); localStorage.setItem(CHAT_KEY_STORE, k); } } return k; } let chatHistory = JSON.parse(localStorage.getItem(CHAT_HIST_STORE) || '[]'); function saveChatHist() { // keep last 40 messages to bound size localStorage.setItem(CHAT_HIST_STORE, JSON.stringify(chatHistory.slice(-40))); if (typeof cloudPushDebounced === 'function') cloudPushDebounced(); } function renderChat() { const el = document.getElementById('chat-messages'); if (!el) return; if (!chatHistory.length) { el.innerHTML = `
输入问题开始 · ⌘↵ 发送 · Esc 取消生成
`; return; } el.innerHTML = chatHistory.map(m => { const isUser = m.role === 'user'; const align = isUser ? 'flex-end' : 'flex-start'; const bg = isUser ? '#2d3142' : '#1a1d26'; const col = isUser ? '#8ab4f8' : '#22c55e'; let ragTag = ''; if (isUser) { const parts = []; if (m._web) parts.push('🌐 web'); if (m._rag) parts.push(`🔎 ${m._rag} 本地`); if (parts.length) ragTag = ` · ${parts.join(' · ')}`; } return `
${isUser?'你':'AI'}${ragTag}
${m.content.replace(/
`; }).join(''); el.scrollTop = el.scrollHeight; } function buildCurrentProfContext() { if (!document.getElementById('chat-include-context')?.checked) return ''; const p = readerState.filtered[readerState.index]; if (!p) return ''; const ctx = { name: p.name, school: p.school, department: p.department, primary_direction: p.primary_direction, research_areas: p.research_areas, reason: p.reason_to_apply || p.reason_to_note, notable_recent_paper: p.notable_recent_paper, current_projects: p.current_projects, junior_pi: p.junior_pi, pi_start_year: p.pi_start_year, summer_availability_score: summerAvailability(p), overall_priority_score: overallScore(p), website: p.website, email: p.email, fit_score: p.fit_score, priority: p.priority, christina_overlap: p.christina_overlap, cold_email_hooks: p.cold_email_hooks, key_papers: p.key_papers_chronological }; return '\n\n---\n你当前在精读的教授档案:\n```json\n' + JSON.stringify(ctx, null, 2) + '\n```\n'; } function buildRAGContext(query) { if (!document.getElementById('chat-use-rag')?.checked) return { text:'', count:0 }; const k = parseInt(document.getElementById('chat-rag-k')?.value || '15', 10); const hits = ragSearch(query, k); if (!hits.length) return { text:'', count:0 }; const text = '\n\n---\n🔎 本地 RAG 检索 — 你的 tracker 内最相关的 ' + hits.length + ' 条(按相关度排序):\n\n' + hits.map((d, i) => { let summary; if (d.kind === 'prof') { const p = d.ref; summary = `【教授】${p.name||''} · ${p.school||''} · ${p.department||''} · ${p.primary_direction||''} 研究:${(p.research_areas||[]).join(', ')} 状态:${p.recruiting_status||''} · junior=${p.junior_pi||false} · 暑期=${summerAvailability(p)}/10 · 综合=${overallScore(p)} 关键:${(p.reason_to_apply || p.reason_to_note || '').substring(0,300)} 论文:${(p.notable_recent_paper||'').substring(0,200)} 邮箱:${p.email||''}`; } else if (d.kind === 'paper') { const r = d.ref; summary = `【论文】${r.title} (${r.venue} ${r.year} ${r.country||''}) 作者:${(r.authors||[]).slice(0,4).join(', ')} 机构:${r.institution||''} · 方向:${r.direction||''} 摘要:${r.summary||''} 价值:${r.why_important||''} 链接:${r.link||''}`; } else if (d.kind === 'industry') { const r = d.ref; summary = `【工业发布】${r.year}-${r.month||''} ${r.org}: ${r.name} 类型:${r.type||''} 说明:${r.summary||''} 链接:${r.link||''}`; } else if (d.kind === 'venue') { const v = d.ref; summary = `【会议】${v.key} (${v.full}) · ${v.area} · ${v.tier} · accept ${v.accept_rate||''} · ddl ${v.deadline||''} 趋势:${v.trend||''}`; } else if (d.kind === 'comparison') { const c = d.ref; summary = `【中外对比】${c.direction} 美国:${c.us_strength||''} 国内:${c.china_strength||''} 差距:${c.gap||''} 代表 US:${(c.examples_us||[]).join(', ')} 代表 CN:${(c.examples_cn||[]).join(', ')}`; } else { summary = JSON.stringify(d.ref); } return `[${i+1}] ${summary}`; }).join('\n\n'); return { text, count: hits.length }; } async function sendChat() { const input = document.getElementById('chat-input'); const sendBtn = document.getElementById('chat-send'); const userMsg = input.value.trim(); if (!userMsg) return; const key = getChatKey(); if (!key) return; const model = document.getElementById('chat-model').value; const ragCtx = buildRAGContext(userMsg); const useWebUI = document.getElementById('chat-use-web')?.checked; const fullUser = userMsg + ragCtx.text + buildCurrentProfContext(); chatHistory.push({ role:'user', content: userMsg, _rag: ragCtx.count, _web: useWebUI }); let status = '⏳ '; if (useWebUI) status += '联网搜索中'; else if (ragCtx.count) status += `本地检索 ${ragCtx.count} 条,正在思考`; else status += '思考中'; const placeholder = { role:'assistant', content: status + '…' }; chatHistory.push(placeholder); renderChat(); input.value = ''; sendBtn.disabled = true; // Use the RAG-augmented user message for LAST user turn only; // older turns keep their original short content (saves tokens) const lastUserIdx = chatHistory.length - 2; const messages = [ { role:'system', content: CHAT_SYS_PROMPT + (ragCtx.count ? '\n\n你将收到 RAG 检索结果,请优先基于这些条目回答,引用时用 [编号]。如果检索结果不够,明说"我的库里没有,以下是推测"。' : '') }, ...chatHistory.slice(0,-1).map((m, i) => { if (i === lastUserIdx && m.role === 'user') { return { role:'user', content: fullUser }; } return { role: m.role, content: m.content }; }) ]; try { const useWeb = document.getElementById('chat-use-web')?.checked; const isReasoner = /^o[0-9]/.test(model); // If web search requested, route to search-preview models (they alone do native web search) let actualModel = model; if (useWeb) { if (model.startsWith('gpt-4o-mini')) actualModel = 'gpt-4o-mini-search-preview'; else if (model.startsWith('gpt-4o') || model.startsWith('gpt-4.1') || isReasoner) actualModel = 'gpt-4o-search-preview'; } const isSearchModel = actualModel.includes('-search-preview'); const body = { model: actualModel, messages }; if (!isReasoner && !isSearchModel) body.temperature = 0.4; if (isReasoner) body.max_completion_tokens = 4000; else body.max_tokens = 2500; // search-preview models don't accept temperature; they accept web_search_options if (isSearchModel) { body.web_search_options = { search_context_size: 'medium' }; } const resp = await fetch('https://api.openai.com/v1/chat/completions', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+key}, body: JSON.stringify(body) }); if (!resp.ok) { const err = await resp.text(); placeholder.content = '❌ API 错误 ' + resp.status + ' (model=' + actualModel + '):\n' + err.substring(0, 600); } else { const data = await resp.json(); const msg = data.choices?.[0]?.message; let content = msg?.content || '(空响应)'; // Append citations if present (search-preview returns annotations) const annots = msg?.annotations || []; if (annots.length) { const cites = annots .filter(a => a.type === 'url_citation' && a.url_citation?.url) .map((a,i) => `[${i+1}] ${a.url_citation.title || a.url_citation.url}\n ${a.url_citation.url}`); if (cites.length) content += '\n\n📎 来源:\n' + cites.join('\n'); } content += '\n\n— 模型: ' + actualModel + (isSearchModel ? ' 🌐' : ''); placeholder.content = content; } } catch (e) { placeholder.content = '❌ 网络错误: ' + e.message; } saveChatHist(); renderChat(); sendBtn.disabled = false; } // Event listeners document.getElementById('chat-send')?.addEventListener('click', sendChat); document.getElementById('chat-input')?.addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); sendChat(); } }); document.getElementById('chat-clear-key')?.addEventListener('click', () => { if (confirm('清除浏览器内保存的 API key?')) { localStorage.removeItem(CHAT_KEY_STORE); alert('已清除。下次问问题会重新提示输入。'); } }); document.getElementById('chat-clear-hist')?.addEventListener('click', () => { if (confirm('清空对话历史?')) { chatHistory = []; saveChatHist(); renderChat(); } }); document.getElementById('chat-model')?.addEventListener('change', e => { document.getElementById('chat-model-label').textContent = '· ' + e.target.value; localStorage.setItem('prof_tracker_chat_model', e.target.value); }); // Restore last-used model const savedModel = localStorage.getItem('prof_tracker_chat_model'); if (savedModel) { const sel = document.getElementById('chat-model'); if (sel && [...sel.options].some(o=>o.value===savedModel)) { sel.value = savedModel; document.getElementById('chat-model-label').textContent = '· ' + savedModel; } } renderChat(); // Simple modal for a Chinese prof function openChinaModal(p) { const body = document.getElementById('modal-body'); body.innerHTML = `

${p.name || p.id} · ${p.school || ''}

${p.department || ''}

${p.website ? `

${p.website}

` : ''} ${p.email ? `

📧 ${p.email}

` : ''} ${p.research_areas ? `

研究方向

${p.research_areas.map(a=>`${a}`).join('')}
` : ''} ${p.reason_to_note ? `

关注理由

${p.reason_to_note}

` : ''} ${p.notable_recent_paper ? `

近期重要论文

${p.notable_recent_paper}

` : ''} ${p.current_projects ? `

当前项目

` : ''} ${p.notes ? `

备注

${p.notes}

` : ''} `; document.getElementById('modal').classList.add('open'); }